﻿//
// (C) Copyright 2009 Irantha Suwandarathna (iranthas@hotmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#if !SILVERLIGHT
using System.Data.Common;
using System.Data;
#endif
using EffiProz.Core.Errors;
using EffiProz.Core.Lib;
using EffiProz.Core.Store;


namespace EffiProz.Core.DataTypes
{
    public class BitType : BinaryType
    {
        public const long maxBitPrecision = 1024;

        public BitType(int type, long precision)
            : base(type, precision)
        {

        }

        public override int displaySize()
        {
            return (int)precision;
        }

        public override int getAdoTypeCode()
        {
#if !SILVERLIGHT
            return (int)DbType.Binary;
#else
            return -1;
#endif
        }

        public override String getCSharpClassName()
        {
            return "System.Byte[]";
        }

        public override int getSQLGenericTypeCode()
        {
            return typeCode;
        }

        public override String getNameString()
        {
            return typeCode == Types.SQL_BIT ? Tokens.T_BIT
                                             : "BIT VARYING";
        }

        public override String getDefinition()
        {

            if (precision == 0)
            {
                return getNameString();
            }

            StringBuilder sb = new StringBuilder(16);

            sb.Append(getNameString());
            sb.Append('(');
            sb.Append(precision);
            sb.Append(')');

            return sb.ToString();
        }

        public override bool isBitType()
        {
            return true;
        }

        public override long getMaxPrecision()
        {
            return maxBitPrecision;
        }

        public override bool requiresPrecision()
        {
            return typeCode == Types.SQL_BIT_VARYING;
        }

        public override SqlType getAggregateType(SqlType other)
        {

            if (typeCode == other.typeCode)
            {
                return precision >= other.precision ? this
                                                    : other;
            }

            switch (other.typeCode)
            {

                case Types.SQL_ALL_TYPES:
                    return this;

                case Types.SQL_BIT:
                    return precision >= other.precision ? this
                                                        : getBitType(typeCode,
                                                        other.precision);

                case Types.SQL_BIT_VARYING:
                    return other.precision >= precision ? other
                                                        : getBitType(
                                                        other.typeCode, precision);

                case Types.SQL_BINARY:
                case Types.SQL_VARBINARY:
                case Types.SQL_BLOB:
                    return other;

                default:
                    throw Error.error(ErrorCode.X_42562);
            }
        }

        /**
         * Returns type for concat
         */
        public override SqlType getCombinedType(SqlType other, int operation)
        {

            if (operation != OpTypes.CONCAT)
            {
                return getAggregateType(other);
            }

            SqlType newType;
            long newPrecision = precision + other.precision;

            switch (other.typeCode)
            {

                case Types.SQL_ALL_TYPES:
                    return this;

                case Types.SQL_BIT:
                    newType = this;
                    break;

                case Types.SQL_BIT_VARYING:
                    newType = other;
                    break;

                case Types.SQL_BINARY:
                case Types.SQL_VARBINARY:
                case Types.SQL_BLOB:
                    return other.getCombinedType(this, operation);

                default:
                    throw Error.error(ErrorCode.X_42562);
            }

            if (newPrecision > maxBitPrecision)
            {
                if (typeCode == Types.SQL_BIT)
                {

                    // Standard disallows type length reduction
                    throw Error.error(ErrorCode.X_42570);
                }

                newPrecision = maxBitPrecision;
            }

            return getBitType(newType.typeCode, newPrecision);
        }

        public override int compare(Session session, Object a, Object b)
        {

            int i = base.compare(session, a, b);

            if (i == 0)
            {
                if (((BinaryData)a).bitLength(null)
                        == ((BinaryData)b).bitLength(null))
                {
                    return 0;
                }

                return ((BinaryData)a).bitLength(null)
                       > ((BinaryData)b).bitLength(null) ? 1
                                                          : -1;
            }

            return i;
        }

        public override Object convertToTypeLimits(SessionInterface session, Object a)
        {
            return castOrConvertToType(null, a, this, false);
        }

        public override Object castToType(SessionInterface session, Object a,
                                 SqlType otherType)
        {
            return castOrConvertToType(session, a, otherType, true);
        }

        public override Object convertToType(SessionInterface session, Object a,
                                    SqlType otherType)
        {
            return castOrConvertToType(session, a, otherType, false);
        }

        Object castOrConvertToType(SessionInterface session, Object a,
                                   SqlType otherType, bool cast)
        {

            BlobData b;

            if (a == null)
            {
                return null;
            }

            switch (otherType.typeCode)
            {

                case Types.SQL_VARCHAR:
                case Types.SQL_CHAR:
                    {
                        b = session.getScanner().convertToBit((String)a);
                        otherType = getBitType(Types.SQL_BIT_VARYING,
                                               b.length(session));

                        break;
                    }
                case Types.SQL_BIT:
                case Types.SQL_BIT_VARYING:
                case Types.SQL_BINARY:
                case Types.SQL_VARBINARY:
                case Types.SQL_BLOB:
                    b = (BlobData)a;
                    break;

                case Types.SQL_BOOLEAN:
                    {
                        if (precision != 1)
                        {
                            throw Error.error(ErrorCode.X_22501);
                        }

                        if (Convert.ToBoolean(a))
                        {
                            return BinaryData.singleBitOne;
                        }
                        else
                        {
                            return BinaryData.singleBitZero;
                        }
                    }
                case Types.TINYINT:
                case Types.SQL_SMALLINT:
                case Types.SQL_INTEGER:
                case Types.SQL_BIGINT:
                case Types.SQL_REAL:
                case Types.SQL_FLOAT:
                case Types.SQL_DOUBLE:
                case Types.SQL_NUMERIC:
                case Types.SQL_DECIMAL:
                    {
                        if (precision != 1)
                        {
                            throw Error.error(ErrorCode.X_22501);
                        }

                        if (((NumberType)otherType).compareToZero(a) == 0)
                        {
                            return BinaryData.singleBitZero;
                        }
                        else
                        {
                            return BinaryData.singleBitOne;
                        }
                    }
                default:
                    throw Error.error(ErrorCode.X_22501);
            }

            // no special 0 bit consideration
            if (b.bitLength(session) > precision)
            {
                if (!cast)
                {
                    throw Error.error(ErrorCode.X_22001);
                }

                session.addWarning(Error.error(ErrorCode.W_01004));
            }

            int bytePrecision = (int)((precision + 7) / 8);

            if (otherType.typeCode == Types.SQL_BLOB)
            {
                byte[] bytes = b.getBytes(session, 0, bytePrecision);

                b = new BinaryData(bytes, precision);
            }

            switch (typeCode)
            {

                case Types.SQL_BIT:
                    {
                        if (b.bitLength(session) == precision)
                        {
                            return b;
                        }

                        if (b.length(session) > bytePrecision)
                        {
                            byte[] data = b.getBytes(session, 0, bytePrecision);

                            b = new BinaryData(data, precision);
                        }
                        else if (b.length(session) <= bytePrecision)
                        {
                            byte[] data = (byte[])ArrayUtil.resizeArray(b.getBytes(),
                                bytePrecision);

                            b = new BinaryData(data, precision);
                        }

                        break;
                    }
                case Types.SQL_BIT_VARYING:
                    {
                        if (b.bitLength(session) <= precision)
                        {
                            return b;
                        }

                        if (b.length(session) > bytePrecision)
                        {
                            byte[] data = b.getBytes(session, 0, bytePrecision);

                            b = new BinaryData(data, precision);
                        }

                        break;
                    }
                default:
                    throw Error.error(ErrorCode.X_22501);
            }

            byte[] data1 = b.getBytes();

            for (int i = (int)precision; i < b.length(session) * 8; i++)
            {
                BitMap.unset(data1, i);
            }

            return b;
        }

        public override Object convertToDefaultType(SessionInterface session, Object a)
        {

            if (a == null)
            {
                return a;
            }

            if (a is byte[])
            {
                BinaryData data = new BinaryData((byte[])a, ((byte[])a).Length);

                return convertToTypeLimits(session, data);
            }
            else if (a is BinaryData)
            {
                return convertToTypeLimits(session, a);
            }
            else if (a is String)
            {
                return convertToType(session, a, SqlType.SQL_VARCHAR);
            }
            else if (a is Boolean)
            {
                return convertToType(session, a, SqlType.SQL_BOOLEAN);
            }
            else if (a is int)
            {
                return convertToType(session, a, SqlType.SQL_INTEGER);
            }
            else if (a is long)
            {
                return convertToType(session, a, SqlType.SQL_BIGINT);
            }

            throw Error.error(ErrorCode.X_22501);
        }

        public override String convertToString(Object a)
        {

            if (a == null)
            {
                return null;
            }

            return StringConverter.byteArrayToBitString(
                ((BinaryData)a).getBytes(),
                (int)((BinaryData)a).bitLength(null));
        }

        public override String convertToSQLString(Object a)
        {

            if (a == null)
            {
                return Tokens.T_NULL;
            }

            return StringConverter.byteArrayToSQLHexString(
                ((BinaryData)a).getBytes());
        }

        public override bool canConvertFrom(SqlType otherType)
        {

            return otherType.typeCode == Types.SQL_ALL_TYPES
                   || otherType.isBinaryType()
                   || (precision == 1
                       && (otherType.isIntegralType() || otherType
                           .isBooleanType()) || otherType.isCharacterType());
        }

        /** @todo - implement */
        public long position(SessionInterface session, BlobData data,
                             BlobData otherData, Type otherType, long offset)
        {

            if (data == null || otherData == null)
            {
                return -1L;
            }

            long otherLength = ((BlobData)data).bitLength(session);

            if (offset + otherLength > data.bitLength(session))
            {
                return -1;
            }

            throw Error.runtimeError(ErrorCode.U_S0500, "BitType");
        }

        public override BlobData substring(SessionInterface session, BlobData data,
                                  long offset, long length, bool hasLength)
        {

            long end;
            long dataLength = data.bitLength(session);

            if (hasLength)
            {
                end = offset + length;
            }
            else
            {
                end = dataLength > offset ? dataLength
                                          : offset;
            }

            if (end < offset)
            {
                throw Error.error(ErrorCode.X_22011);
            }

            if (offset > end || end < 0)
            {

                // return zero length data
                offset = 0;
                end = 0;
            }

            if (offset < 0)
            {
                offset = 0;
            }

            if (end > dataLength)
            {
                end = dataLength;
            }

            length = end - offset;

            byte[] dataBytes = data.getBytes();
            byte[] bytes = new byte[(int)(length + 7) / 8];

            for (int i = (int)offset; i < end; i++)
            {
                if (BitMap.isSet(dataBytes, i))
                {
                    BitMap.set(bytes, i - (int)offset);
                }
            }

            return new BinaryData(bytes, length);
        }

        int getRightTrimSize(BinaryData data)
        {

            int i = (int)data.bitLength(null) - 1;
            byte[] bytes = data.getBytes();

            for (; i >= 0; i--)
            {
                if (BitMap.isSet(bytes, i))
                {
                    break;
                }
            }

            return i + 1;
        }

        public override BlobData overlay(Session session, BlobData value, BlobData overlay,
                                long offset, long length, bool hasLength)
        {

            if (value == null || overlay == null)
            {
                return null;
            }

            if (!hasLength)
            {
                length = ((BlobData)overlay).bitLength(session);
            }

            switch (typeCode)
            {

                case Types.SQL_BIT:
                case Types.SQL_BIT_VARYING:
                    {
                        byte[] data =
                            (byte[])ArrayUtil.duplicateArray(value.getBytes());
                        byte[] overlaydata = overlay.getBytes();

                        for (int i = 0, pos = (int)offset; i < length;
                                pos += 8, i++)
                        {
                            int count = 8;

                            if (length - pos < 8)
                            {
                                count = (int)length - pos;
                            }

                            BitMap.overlay(data, pos, overlaydata[i], count);
                        }

                        BinaryData binary = new BinaryData(data,
                                                           value.bitLength(session));

                        return binary;
                    }
                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "BitType");
            }
        }

        public override Object concat(Session session, Object a, Object b)
        {

            if (a == null || b == null)
            {
                return null;
            }

            long length = ((BlobData)a).bitLength(session)
                          + ((BlobData)b).bitLength(session);

            if (length > int.MaxValue)
            {
                throw Error.error(ErrorCode.W_01000);
            }

            byte[] aData = ((BlobData)a).getBytes();
            byte[] bData = ((BlobData)b).getBytes();
            int aLength = (int)((BlobData)a).bitLength(session);
            int bLength = (int)((BlobData)b).bitLength(session);
            byte[] bytes = new byte[(int)(length + 7) / 8];

            Array.Copy(aData, 0, bytes, 0, aData.Length);

            for (int i = 0; i < bLength; i++)
            {
                if (BitMap.isSet(bData, i))
                {
                    BitMap.set(bytes, aLength + i);
                }
            }

            return new BinaryData(bytes, length);
        }

        public static BinaryType getBitType(int type, long precision)
        {

            switch (type)
            {

                case Types.SQL_BIT:
                case Types.SQL_BIT_VARYING:
                    return new BitType(type, precision);

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "BitType");
            }
        }
    }
}
